<?php
require_once "Zend/Tokenizer/Interface.php";

/**
 * @category   Zend
 * @package    Zend_Tokenizer
 * @copyright  Salomon BRYS
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Tokenizer_Serialized_XML implements Zend_Tokenizer_Interface
{
	/**
	 * @var Zend_Tokenizer_Interface
	 */
	private $_producer;

	/**
	 * @var string
	 */
	private $_data;

	/**
	 * @var array[]mixed
	 */
	private $_tokens = array();

	/**
	 * @var resource
	 */
	private $_parser;

	const TYPE_CHARACTER_DATA         = 'CHARACTER_DATA';
	const TYPE_START_ELEMENT          = 'ELEMENT_START';
	const TYPE_END_ELEMENT            = 'ELEMENT_END';
	const TYPE_START_NAMESPACE        = 'START_NAMESPACE';
	const TYPE_END_NAMESPACE          = 'END_NAMESPACE';
	const TYPE_EXTERNAL_ENTITY_REF    = 'EXTERNAL_ENTITY_REFERENCE';
	const TYPE_NOTATION               = 'NOTATION';
	const TYPE_PROCESSING_INSTRUCTION = 'PROCESSING_INSTRUCTION';
	const TYPE_UNPARSED_ENTITY        = 'UNPARSED_ENTITY';
	const TYPE_DOM                    = 'DOM';

	/**
	 * @var array[]string
	 */
	private $_tokenTypes = array();

	/**
	 * @var array[]int
	 */
	private $_domLevels = array();

	/**
	 * @var int
	 */
	private $_currentLevel = 0;

	/**
	 * @var array[]int
	 */
	private $_indexSaves = array();

	/**
	 * @var int
	 */
	private $_lastIndex = 0;

	/**
	 * @var int
	 */
	private $_indexDelta = 0;

	/**
	 * Constructor
	 *
	 * @param Zend_Tokenizer_Interface $producer
	 */
	public function __construct(Zend_Tokenizer_Interface $producer, $nsSupport = false, $tokenTypes = array(self::TYPE_DOM), $domLevels = array(0))
	{
		if (!$producer->isStringToken()) {
			require_once 'Zend/Exception.php';
			throw new Zend_Exception("Zend_Tokenizer_XMLStream needs a string token producer.");
		}
		$this->_producer = $producer;

		if ($nsSupport) {
			$this->_parser = xml_parser_create_ns();
		} else {
			$this->_parser = xml_parser_create();
		}

		$this->_tokenTypes = (array)$tokenTypes;
		$this->_domLevels = (array)$domLevels;
		$this->_registerHandlers();
	}

	/**
	 * @see Zend/Tokenizer/Zend_Tokenizer_Interface::getRawData()
	 */
	public function getRawData()
	{
		return $this->_producer->getRawData();
	}

	/**
	 * @see Zend/Tokenizer/Zend_Tokenizer_Interface::hasToken()
	 */
	public function hasToken()
	{
		$this->_fillData();
		return !empty($this->_tokens);
	}

	/**
	 * @see Zend/Tokenizer/Zend_Tokenizer_Interface::getToken()
	 */
	public function getToken()
	{
		$this->_fillData();
		return array_shift($this->_tokens);
	}

	/**
	 * @see Zend/Tokenizer/Zend_Tokenizer_Interface::isEndOfStream()
	 */
	public function isEndOfStream()
	{
		return $this->_producer->isEndOfStream();
	}

	/**
	 * @see Zend/Tokenizer/Zend_Tokenizer_Interface::isStringToken()
	 */
	public function isStringToken()
	{
		return false;
	}

	/**
	 * @see Zend/Tokenizer/Zend_Tokenizer_Interface::getProducer()
	 */
	public function getProducer()
	{
		return $this->_producer->getProducer();
	}

	private function _fillData()
	{
		while ($this->_producer->hasToken())
		{
			$data = $this->_producer->getToken();
			if (in_array(self::TYPE_DOM, $this->_tokenTypes)) {
				$this->_data .= $data;
			}
			xml_parse($this->_parser, $data, $this->isEndOfStream());
		}
	}

	private function _registerHandlers()
	{
		xml_set_object($this->_parser, $this);

		if (in_array(self::TYPE_DOM, $this->_tokenTypes) || in_array(self::TYPE_START_ELEMENT, $this->_tokenTypes) || in_array(self::TYPE_END_ELEMENT, $this->_tokenTypes)) {
			xml_set_element_handler($this->_parser, '_startElementHandler', '_endElementHandler');
		}
		if (in_array(self::TYPE_CHARACTER_DATA, $this->_tokenTypes)) {
			xml_set_character_data_handler($this->_parser, '_characterDataHandler');
		}
		if (in_array(self::TYPE_START_NAMESPACE, $this->_tokenTypes)) {
			xml_set_start_namespace_decl_handler($this->_parser, '_startNamespaceHandler');
		}
		if (in_array(self::TYPE_END_NAMESPACE, $this->_tokenTypes)) {
			xml_set_end_namespace_decl_handler($this->_parser, '_endNamespaceHandler');
		}
		if (in_array(self::TYPE_EXTERNAL_ENTITY_REF, $this->_tokenTypes)) {
			xml_set_external_entity_ref_handler($this->_parser, '_externalEntityRefHandler');
		}
		if (in_array(self::TYPE_NOTATION, $this->_tokenTypes)) {
			xml_set_notation_decl_handler($this->_parser, '_notationHandler');
		}
		if (in_array(self::TYPE_PROCESSING_INSTRUCTION, $this->_tokenTypes)) {
			xml_set_notation_decl_handler($this->_parser, '_processingInstructionHandler');
		}
		if (in_array(self::TYPE_UNPARSED_ENTITY, $this->_tokenTypes)) {
			xml_set_notation_decl_handler($this->_parser, '_unparsedEntityHandler');
		}
	}

	private function _mkToken($type, $value)
	{
		if (!in_array($type, $this->_tokenTypes))
			return ;

		if (count($this->_tokenTypes) > 1) {
			$this->_tokens[] = array('type' => $type, 'value' => $value);
		} else {
			$this->_tokens[] = $value;
		}
	}

	private function _startElementHandler($parser, $name, $attribs)
	{
		if (in_array(self::TYPE_DOM, $this->_tokenTypes)) {
			if (in_array($this->_currentLevel, $this->_domLevels)) {
				array_push($this->_indexSaves, $this->_lastIndex);
			}
			++$this->_currentLevel;
			$this->_lastIndex = xml_get_current_byte_index($this->_parser) + 1;
		}
		$this->_mkToken(self::TYPE_START_ELEMENT, array('name' => $name, 'attribs' => $attribs));
	}

	private function _endElementHandler($parser, $name)
	{
		if (in_array(self::TYPE_DOM, $this->_tokenTypes)) {
			$this->_lastIndex = xml_get_current_byte_index($this->_parser);
			--$this->_currentLevel;
			if (in_array($this->_currentLevel, $this->_domLevels)) {
				$startIndex = array_pop($this->_indexSaves);
				$ret = new DOMDocument();
				$ret->loadXML(trim(substr($this->_data, $startIndex - $this->_indexDelta, $this->_lastIndex - $startIndex)));
				$this->_mkToken(self::TYPE_DOM, $ret);
				if (empty($this->_indexSaves)) {
					$this->_data = substr($this->_data, $this->_lastIndex - $this->_indexDelta);
					$this->_indexDelta = $this->_lastIndex;
				}
			}
		}
		$this->_mkToken(self::TYPE_END_ELEMENT, $name);
	}

	private function _characterDataHandler($parser, $data)
	{
		$this->_mkToken(self::TYPE_CHARACTER_DATA, $data);
	}

	private function _startNamespaceHandler($parser, $user_data, $prefix)
	{
		$this->_mkToken(self::TYPE_START_NAMESPACE, array('user_data' => $user_data, 'prefix' => $prefix));
	}

	private function _endNamespaceHandler($parser, $user_data, $prefix)
	{
		$this->_mkToken(self::TYPE_END_NAMESPACE, array('user_data' => $user_data, 'prefix' => $prefix));
	}

	private function _externalEntityRefHandler($parser, $open_entity_names, $base, $system_id, $public_id)
	{
		$this->_mkToken(self::TYPE_EXTERNAL_ENTITY_REF, array('open_entity_names' => $open_entity_names, 'base' => $base, 'system_id' => $system_id, 'public_id' => $public_id));
	}

	private function _notationHandler($parser, $notation_name, $base, $system_id, $public_id)
	{
		$this->_mkToken(self::TYPE_NOTATION, array('notation_name' => $notation_name, 'base' => $base, 'system_id' => $system_id, 'public_id' => $public_id));
	}

	private function _processingInstructionHandler($parser, $target, $data)
	{
		$this->_mkToken(self::TYPE_PROCESSING_INSTRUCTION, array('target' => $target, 'data' => $data));
	}

	private function _unparsedEntityHandler($parser, $entity_name, $base, $system_id, $public_id, $notation_name)
	{
		$this->_mkToken(self::TYPE_UNPARSED_ENTITY, array('entity_name' => $entity_name, 'base' => $base, 'system_id' => $system_id, 'public_id' => $public_id, 'notation_name' => $notation_name));
	}
}
